/* EasyWay Game Engine
 * Copyright (C) 2006 Daniele Paggi.
 *  
 * Written by: 2006 Daniele Paggi<dshnt@hotmail.com>
 *   
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Library General Public License for more details.
 * 
 * You should have received a copy of the GNU Library General Public
 * License along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.easyway.tiles;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Iterator;

import org.easyway.annotations.Optimized;
import org.easyway.annotations.editor.Creator;
import org.easyway.annotations.editor.Info;
import org.easyway.annotations.editor.Resource;
import org.easyway.collisions.CollisionUtils;
import org.easyway.collisions.SingleCollisionList;
import org.easyway.interfaces.base.ITexture;
import org.easyway.interfaces.extended.IDrawing;
import org.easyway.interfaces.extended.ILayerID;
import org.easyway.interfaces.sprites.ISpriteColl;
import org.easyway.lists.BaseList;
import org.easyway.objects.Plain2D;
import org.easyway.objects.sprites2D.Mask;
import org.easyway.objects.sprites2D.StaticFullyMask;
import org.easyway.objects.texture.TextureCompact;
import org.easyway.system.StaticRef;
import org.easyway.utils.Utility;

/**
 * implements the tile engine<br>
 * example:<br>
 * ...<br>
 * TileManager tm = new TileManger( 1000, 1000, 64, 64, true, 3 );<br>
 * Texture image = new Texture("path.png");<br>
 * Tile t = new Tile(image);<br>
 * tm.setTile( 0, 0, t );<br>
 * tm.setTile( 1, 0, t );<br>
 * ...<br>
 * <br>
 */
@Optimized
//
public class TileManager extends Plain2D implements IDrawing, ISpriteColl,
		ILayerID {

	private static final long serialVersionUID = -7576756624211903331L;

	/** depth of TileManager */
	private int layer = 0;

	/**
	 * the drawing sheet
	 */
	private int idLayer = -1;

	/** number of tiles minus one */
	private int numX, numY;

	/** sizes of tiles */
	private int tileWidth, tileHeight;

	/** an array that record the used tiles */
	public Tile grid[][];

	public boolean isPrecise = true;

	/**
	 * indicates if the object is added or not to the auto-collisionLists.
	 */
	protected boolean collAdded = false;

	// protected float x, y;

	/** records that objects has gone a collision with a tile of tile manager */
	protected ArrayList<ISpriteColl> collisions;

	/**
	 * the tileMap mask
	 */
	protected StaticFullyMask mask;

	protected TiledCollisionList collisionList;

	/**
	 * indicates if the tilemanager is to show at screen or not
	 */
	protected boolean visible = true;

	/**
	 * the tileSet that the TileManager use
	 */
	protected TileSet tileSet;

	/**
	 * creates a new instance of tileMap
	 * 
	 * @param width
	 *            width of tile manager: number of tile across the X axis
	 * @param height
	 *            height of tile manager: number of tile across the Y axis
	 * @param tileWidth
	 *            the size of tile used by the tile manager
	 * @param tileHeight
	 *            the size of tile used by the tile manager
	 * @param name
	 *            the name of the TiledMap
	 */
	public TileManager(int width, int height, int tileWidth, int tileHeight,
			String name) {
		this(width, height, tileWidth, tileHeight, true, 2, name);
	}

	/**
	 * creates a new instance of tileMap
	 * 
	 * @param width
	 *            width of tile manager: number of tile across the X axis
	 * @param height
	 *            height of tile manager: number of tile across the Y axis
	 * @param tileWidth
	 *            the size of tile used by the tile manager
	 * @param tileHeight
	 *            the size of tile used by the tile manager
	 * @param autoAdd
	 *            auto adds the TileManager to the engin list (for example the
	 *            drawing list)
	 * @param idlayer
	 *            the id of the drawing sheet
	 * @param name
	 *            the name of the TiledMap
	 */
	@Creator("create empty")
	//
	public TileManager(@Info("width")
	int width, @Info("height")
	int height, @Info("tile width")
	int tileWidth, @Info("tile height")
	int tileHeight, @Info("to add")
	boolean autoAdd, @Info("id latyer")
	int idLayer, @Info("name")
	String name) {
		super(autoAdd);
		// StaticRef.layers[idlayer].add(this);
		setIdLayer(idLayer);
		type.set(name);
		this.numX = width;// - 1;
		this.numY = height;// - 1;
		this.width = width * tileWidth;
		this.height = height * tileHeight;
		this.tileHeight = tileHeight;
		this.tileWidth = tileWidth;
		grid = new Tile[width][height];
		collisions = new ArrayList<ISpriteColl>(100);
		mask = new StaticFullyMask((numX - 1) * tileWidth, (numY - 1)
				* tileHeight);
		collisionList = new TiledCollisionList(this);
		StaticRef.coreCollision.tileGroups.add(this);
	}

	/**
	 * create a new instance of TileManager loading it from a file
	 * 
	 * @param file
	 *            the file that contains all the information about the tile
	 *            manager
	 */
	@Creator("load from file")
	//
	public TileManager(@Resource("file")
	String file, @Info("auto added")
	boolean autoAdd, @Info("id Layer")
	int idLayer, @Info("Type name")
	String tname) {
		super(autoAdd);
		setIdLayer(idLayer);
		type.set(tname);

		InputStream inStream; // generic stream to the file
		ObjectInputStream objStream; // stream for objects to the file
		try {
			if (file.startsWith("/"))
				file = file.substring(1);
			while (file.indexOf("\\") != -1)
				file = file.substring(0, file.indexOf("\\")) + "/"
						+ file.substring(file.indexOf("\\") + 1);

			inStream = Thread.currentThread().getContextClassLoader()
					.getResource(file).openStream();
			// inStream = new FileInputStream(file);
			objStream = new ObjectInputStream(inStream);
			// because I'm stupid I've not inserted the VERSION-id in the
			// beginning of the file
			// but I can use the first integer value for understand if it's and
			// old-version file or a new one
			// the first integer is the numX and it MUST be > 0
			// then in the new version It will be < 0
			int retroCompatibilty = objStream.readInt();
			if (retroCompatibilty > 0) { // old version
				readFromFile0360(objStream, retroCompatibilty);
			} else { // new version
				readFromFile(objStream,file);
			}

			objStream.close();
			inStream.close();
			Utility.error("Successfully TileManager Loaded",
					"TileManager(String)OK");
		} catch (Exception e) {
			Utility.error("Error on loading the TileManager",
					"TileManager(String)", e);
		}

		collisions = new ArrayList<ISpriteColl>(100);
		mask = new StaticFullyMask((numX - 1) * tileWidth, (numY - 1)
				* tileHeight);
		collisionList = new TiledCollisionList(this);
		StaticRef.coreCollision.tileGroups.add(this);
	}

	protected void readFromFile0360(ObjectInputStream objStream, int firstValue)
			throws Exception {
		this.numX = firstValue;// objStream.readInt();
		this.numY = objStream.readInt();
		this.tileWidth = objStream.readInt();
		this.tileHeight = objStream.readInt();
		this.width = numX * tileWidth;
		this.height = numY * tileHeight;
		grid = new Tile[numX][numY];

		// number of images
		int num = objStream.readInt();
		// System.out.println("num"+num);
		String name, type;
		Tile tileList[] = new Tile[num];
		ITexture text;
		for (int i = 0; i < num; ++i) {
			name = (String) objStream.readObject();
			type = (String) objStream.readObject();
			// System.out.println(type);
			(tileList[i] = new Tile(text = new TextureCompact(name, 255, 255,
					255))).getType().set(type);
			TileManagerEditor.tiles.add(tileList[i]);
			StaticRef.textures.add(text); // for the editor
			new Mask(text);
		}

		for (int i = 0; i < numX; ++i)
			for (int j = 0; j < numY; ++j) {
				num = (byte) objStream.readByte();
				if (num != (byte) -1) {
					grid[i][j] = tileList[num];
				}
			}
	}

	/**
	 * sets a tile on the coordinates x,y
	 * 
	 * @param x
	 *            the x coordinate
	 * @param y
	 *            the y coordinate
	 * @param tile
	 *            the tile to sets
	 */
	public boolean setTile(int x, int y, Tile tile) {
		if (x < 0 || x >= numX) {
			Utility.error("x out of bounds",
					"TileManager.setTile(int,int,Tile)");
			return false;
		}
		if (y < 0 || y >= numY) {
			Utility.error("y out of bounds",
					"TileManager.setTile(int,int,Tile)");
			return false;
		}
		grid[x][y] = tile;
		return true;
	}

	public void render() {
		if (!visible)
			return;
		int startx = Math.max(0, (int) StaticRef.getCamera().x / tileWidth);

		if (startx >= numX) {
			return;
		}
		int endx = Math.min(numX - 1, startx + StaticRef.getCamera().getWidth()
				/ tileWidth + 1);
		if (endx < 0) {
			return;
		}
		if (startx == endx)
			return;

		int starty = Math.max(0, (int) StaticRef.getCamera().y / tileHeight);
		if (starty >= numY)
			return;

		int endy = Math.min(numY - 1, starty
				+ StaticRef.getCamera().getHeight() / tileHeight + 1);
		if (endy < 0)
			return;
		if (starty == endy)
			return;

		int sx = startx, sy;

		float coordstartx = Math.max(-StaticRef.getCamera().x % tileWidth, x
				- StaticRef.getCamera().x);
		float coordstarty = Math.max(-StaticRef.getCamera().y % tileHeight, y
				- StaticRef.getCamera().y);
		float coordendx = (endx - startx) * tileWidth + coordstartx;
		float coordendy = (endy - starty) * tileHeight + coordstarty;

		float cx, cy;
		Tile temp;
		// GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
		for (sy = starty, sx = startx, cy = coordstarty; cy <= coordendy; cy += tileHeight, ++sy) {
			cx = coordstartx;
			sx = startx;
			if ((temp = grid[sx][sy]) != null && temp.image != null) {
				temp.render(cx, cy, tileWidth, tileHeight);
			}
			for (cx += tileWidth, ++sx; cx <= coordendx; cx += tileWidth, ++sx) {
				if ((temp = grid[sx][sy]) != null && temp.image != null) {
					temp.render(cx, cy, tileWidth, tileHeight);
				}
			}

		}

	}

	/**
	 * sets all the tiles to the selected tile
	 * 
	 * @param tile
	 *            the selected tile
	 */
	public void setAllTileTo(Tile tile) {
		for (int i = 0; i < numX; ++i)
			for (int j = 0; j < numY; ++j)
				grid[i][j] = tile;
	}

	public int getLayer() {
		return layer;
	}

	/**
	 * returns the tile at position numX, numY
	 * 
	 * @param numX
	 *            x position of the tile on the TileManager
	 * @param numY
	 *            x position of the tile on the TileManager
	 */
	public Tile getTile(int x, int y) {
		if (x < numX && x >= 0) {
			if (y >= 0 && y < numY) {
				return grid[x][y];
			}
			Utility.error("y out of bounds", "TileManager.getTile(int,int)");
			return null;
		}
		Utility.error("x out of bounds", "TileManager.getTile(int,int)");
		return null;
	}

	/**
	 * returns the number of tiles that the tile manager can manages across the
	 * Y axis
	 */
	public int getNumY() {
		return numY;
	}

	/**
	 * returns the height of tiles managed
	 */
	public int getTileHeight() {
		return tileHeight;
	}

	/**
	 * returns the width of tiles managed
	 */
	public int getTileWidth() {
		return tileWidth;
	}

	/**
	 * returns the number of tiles that the tile manager can manages across the
	 * X axis
	 */
	public int getNumX() {
		return numX;
	}

	/**
	 * change the depth of tile manager
	 */
	public void setLayer(int layer) {
		this.layer = layer;
		readdToDrawingLists();
	}

	/**
	 * change the Height of tile managed
	 * 
	 * @param tileHeight
	 *            the new height to use
	 */
	public void setTileHeight(int tileHeight) {
		this.tileHeight = tileHeight;
		mask = new StaticFullyMask((numX - 1) * tileWidth, (numY - 1)
				* tileHeight);
	}

	/**
	 * change the Width of tile managed
	 * 
	 * @param tileWidth
	 *            the new width to use
	 */
	public void setTileWidth(int tileWidth) {
		this.tileWidth = tileWidth;
		mask = new StaticFullyMask((numX - 1) * tileWidth, (numY - 1)
				* tileHeight);
	}

	public void onCollision() {
		ISpriteColl sprite;
		// ArrayList<ISpriteColl> tempList;
		// int index;
		SingleCollisionList coll;
		TileIterator ti;

		Iterator<ISpriteColl> it = collisions.iterator();

		while (it.hasNext()) {
			sprite = it.next();// collisions.get(i);// (ISpriteColl)
			// collisions.getCurrent();
			assert sprite != null : "errore di inserimento?";

			if ((sprite.getCollisionList()) == null)
				continue;
			coll = new SingleCollisionList(false, sprite);
			coll.isPrecise = this.isPrecise;
			ti = new TileIterator(this, sprite);
			while (ti.hasElement()) {
				if (ti.getCurrent() != null)
					coll.add(ti.getCurrent());
			}
			if (coll.size() > 0) // 0.1.9
				coll.loop();
			// 0.3.x
			// if ((index = tempList.indexOf(this)) != -1) {
			// tempList.remove(index);
			// } else {
			// // XXX:
			// sprite.onCollision();
			// }
		}
		collisions.clear();
	}

	/**
	 * checks if the sprite has or not a collision with a tile of tile manager
	 * 
	 * @param sprite
	 *            the sprite to check
	 */
	public ArrayList<ISpriteColl> testCollision(ISpriteColl sprite) {
		// ArrayList<ISpriteColl> coll = sprite.getCollisionList();
		// if (coll == null) {
		// return null;
		// }
		ArrayList<ISpriteColl> coll = new ArrayList<ISpriteColl>(10);
		if (!CollisionUtils.rectangleHit(this, sprite)) {
			return null;
		}
		TileIterator ti = new TileIterator(this, sprite);
		while (ti.hasElement()) {
			if (ti.getCurrent() != null) {
				if (CollisionUtils.rectangleHit(ti.getCurrent(), sprite)) {
					if (isPrecise) {
						if (CollisionUtils.trueHit(ti.getCurrent(), sprite)) {
							coll.add(ti.getCurrent());
						}
					} else {
						coll.add(ti.getCurrent());
					}
				}
			}
		}
		return coll;
	}

	public ArrayList<ISpriteColl> getCollisionList() {
		return collisions;
	}

	public Mask getMask() {
		assert mask != null;
		return mask;
		// return new StaticFullyMask((numX - 1) * tileWidth, (numY - 1)
		// * tileHeight);
	}

	public float getRotation2D() {
		return 0;
	}

	// -----------------------------------------------------------------
	// ---------------------------- IDLAYER-----------------------------
	// -----------------------------------------------------------------

	public int getIdLayer() {
		return idLayer;
	}

	public void setIdLayer(int id) {
		if (idLayer != -1) {
			StaticRef.layers[idLayer].remove(this);
		}
		if (id < 0) {
			id = 0;
		} else if (id > StaticRef.layers.length) {
			id = StaticRef.layers.length;
		}
		idLayer = id;
		StaticRef.layers[idLayer].add(this);
	}

	public boolean getAddedToCollisionList() {
		return collAdded;
	}

	public void setAddedToCollisionList(boolean value) {
		collAdded = value;
	}

	/**
	 * adds a sprite to the testing-collision list
	 * 
	 * @param sprite
	 */
	public void add(ISpriteColl sprite) {
		collisionList.add(sprite);
	}

	/**
	 * removes a sprite from the testing-collision list
	 * 
	 * @param sprite
	 */
	public void remove(ISpriteColl sprite) {
		collisionList.remove(sprite);
	}

	/**
	 * returns the testing-collision list
	 * 
	 * @return
	 */
	public TiledCollisionList getTestingCollisionList() {
		return collisionList;
	}

	public void setTestingCollisionList(BaseList<ISpriteColl> list) {
		collisionList.setList(list);
	}

	/**
	 * clears the testing-collision list.<br>
	 * note: O( n ); with 'n' = list.size()
	 */
	public void clearTestingCollisionList() {
		collisionList.removeAll();
	}

	public static TileManager getTileManager(String name) {
		return StaticRef.coreCollision.searchTileMap(name);
	}

	public String toString() {
		return getType().toString();
	}

	public boolean isVisible() {
		return visible;
	}

	public void setVisible(boolean visible) {
		this.visible = visible;
	}

	public void readFromFile(ObjectInputStream in,String file) {
		try {
			// read the version (at this time it's only 1)
			/* int version = */in.readInt();
			
			// fields:
			numX = in.readInt();
			numY = in.readInt();
			grid = new Tile[numX][numY];
			
			tileWidth = in.readInt();
			tileHeight = in.readInt();
			setType((String)in.readObject());
			
			String tileSetName = (String)in.readObject();
			tileSet = TileSet.readFromFile(tileSetName);
			Tile tile;
			for (int y = 0; y < numY; ++y) {
				for (int x = 0; x < numX; ++x) {
					//tile = grid[x][y];
					int tileid = in.readInt();
					tile = tileSet.get(tileid);
					grid[x][y] = tile;

				}
			}


		} catch (IOException e) {
			e.printStackTrace();
		} catch(ClassNotFoundException e) {
			e.printStackTrace();
		}
	}

	public void writeOnFile(String filename) {
		try {
			FileOutputStream fout = new FileOutputStream(filename);
			ObjectOutputStream out = new ObjectOutputStream(fout);

			// the -1 indicates that is a new version
			out.writeInt(-1);
			// the VERSION-id

			// fields:
			out.writeInt(1);
			out.writeInt(numX);
			out.writeInt(numY);
			out.writeInt(tileWidth);
			out.writeInt(tileHeight);
			out.writeObject(getType().toString()); // name

			// TODO: write TileSet
			
			//tileSet.writeOnFile(filename+"_tileset");
			out.writeObject(tileSet.pathFilename);
			// grid:
			Tile tile;
			for (int y = 0; y < numY; ++y) {
				for (int x = 0; x < numX; ++x) {
					tile = grid[x][y];

					if (tile != null) {
						// the ID of the tile
						out.writeInt(grid[x][y].id);
					} else { // tile == null
						// write -1 for a null tile
						out.writeInt(-1);
					}
				}
			}
			out.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public TileSet getTileSet() {
		return tileSet;
	}

	public void setTileSet(TileSet tileSet) {
		this.tileSet = tileSet;
	}

}// end class
